﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
using Common.Utilities;
using Common.Utilities.Dispatcher;

namespace Workflows.Components.TextMining.FullTextSearch
{
    /// <summary>
    /// 
    /// </summary>
    public class SearchFieldContributionWorker:IWorker 
    {
        #region Implementation of IWorker

        public event WorkerStarted WorkerStartedEvent;
        public event WorkerFinished WorkerFinishedEvent;
        public event WorkerHasError WorkerErredEvent;
        public event WorkerCancelled WorkerCancelledEvent;
        public event WorkerStatusChanged WorkerStatusChangedEvent;

        private Thread workerThread;
        private SearchFieldContributionInstruction instruction;
        private SearchFieldContributionOutput output;
        private RunContext _Context;
        private Guid _TicketID;
        private string _WorkflowName;
        private string _ComponentName;

        /// <summary>
        /// 
        /// </summary>
        public string ComponentName
        {
            get { return _ComponentName; }
        }

        /// <summary>
        /// 
        /// </summary>
        public string WorkflowName
        {
            get { return _WorkflowName; }
        }

        /// <summary></summary>
        public Thread WorkerThread
        {
            get { return workerThread; }
        }
        /// <summary></summary>
        public InstructionBase Instruction
        {
            get { return instruction; }
        }
        /// <summary></summary>
        public ResultBase Output
        {
            get { return output; }
        }
        /// <summary></summary>
        public Type ComponentType
        {
            get { return typeof(SearchFieldContributionComponent); }
        }
        /// <summary></summary>
        public RunContext Context
        {
            get { return _Context; }
            set { _Context = value; }
        }
        /// <summary></summary>
        public void SetInstruction(Guid ticketGuid, InstructionBase request, string workflowName, string componentName)
        {
            this._TicketID = ticketGuid;
            this.instruction = (SearchFieldContributionInstruction)request;
            this._WorkflowName = workflowName;
            this._ComponentName = componentName;
        }
        /// <summary></summary>
        public void SetInstruction(RunContext context, Guid ticketGuid, InstructionBase request, string workflowName, string componentName)
        {
            this._TicketID = ticketGuid;
            this.instruction = (SearchFieldContributionInstruction)request;
            this._Context = context;
            this._WorkflowName = workflowName;
            this._ComponentName = componentName;
        }
        /// <summary></summary>
        public void DoWork()
        {
            if (this.WorkerStartedEvent != null)
            {
                this.WorkerStartedEvent(this._TicketID, this, this.GetType(), this.instruction);
            }
            this.workerThread = Thread.CurrentThread;
            this.DetermineFieldWeights();
        }
        /// <summary></summary>
        public void DoWorkAsync()
        {
            if (this.WorkerStartedEvent != null)
            {
                this.WorkerStartedEvent(this._TicketID, this, this.GetType(), this.instruction);
            }
            this.workerThread = new Thread(new ThreadStart(this.DetermineFieldWeights));
            this.workerThread.Start();
        }
        /// <summary></summary>
        public void CancelWork()
        {
            if (this.workerThread != null && this.workerThread.IsAlive)
            {
                this.workerThread.Abort();
                this.workerThread.Join();

                if (this.WorkerCancelledEvent != null)
                {
                    this.WorkerCancelledEvent(this._TicketID, this.GetType(), this.instruction);
                }
            }
        }

        #endregion

        #region work
        private void DetermineFieldWeights()
        {
            try
            {
                XmlDocument xDoc = new XmlDocument();
                xDoc.LoadXml("<Result></Result>");
                XmlNode root = xDoc.DocumentElement;
                XmlNode evidenceNode = XmlDataUtil.AddElement(ref xDoc, root, "Evidence");
                List<string> standard = this.GetStandardSetKeys();
                Dictionary<string,double> bestFieldWeights=new Dictionary<string, double>();
                foreach(string fieldName in this.instruction.TextFields)
                {
                    List<FullTextSearchResult> fieldHits=new List<FullTextSearchResult>();
                    List<FullTextSearchResult> otherHits=new List<FullTextSearchResult>();
                    this.DoFullTextSearch(fieldName, ref fieldHits, ref otherHits);
                    Dictionary<double, List<string>> keysFoundAcrossThreshold =
                        this.FilterAcrossWeights(fieldHits, otherHits);
                    XmlNode testRootNode = XmlDataUtil.AddElement(ref xDoc, evidenceNode, "Tests");
                    XmlDataUtil.UpdateAttribute(ref xDoc, testRootNode, "FieldName", fieldName);
                    int iteration = 0;
                    double bestFScore = 0;
                    double bestFieldWeight = 0;
                    foreach(double w in keysFoundAcrossThreshold.Keys)
                    {
                        iteration++;
                        XmlNode testNode = XmlDataUtil.AddElement(ref xDoc, testRootNode, "Test");
                        XmlDataUtil.UpdateAttribute(ref xDoc, testNode,"Iteration",iteration.ToString());
                        XmlDataUtil.UpdateAttribute(ref xDoc, testNode, "FieldWeight", w.ToString());
                        double fScore = this.CalculateFScore(keysFoundAcrossThreshold[w], standard);
                        XmlDataUtil.UpdateAttribute(ref xDoc, testNode, "FScore",fScore.ToString());
                        if(fScore > bestFScore)
                        {
                            bestFScore = fScore;
                            bestFieldWeight = w;
                        }
                    }
                    bestFieldWeights.Add(fieldName, bestFieldWeight);
                }

                double totalWeight = 0;
                foreach(double w in bestFieldWeights.Values)
                {
                    totalWeight += w;
                }
                if (totalWeight == 0)
                    totalWeight = 1;
                string[] fieldNames=new string[bestFieldWeights.Count];
                bestFieldWeights.Keys.CopyTo(fieldNames,0);
                foreach (string fieldName in fieldNames)
                {
                    bestFieldWeights[fieldName] = bestFieldWeights[fieldName]/totalWeight;
                }

                XmlNode fieldRootNode = XmlDataUtil.AddElement(ref xDoc, root, "Fields");
                foreach(string fieldName in bestFieldWeights.Keys)
                {
                    XmlNode fieldNode = XmlDataUtil.AddElement(ref xDoc, fieldRootNode, "Field");
                    XmlDataUtil.UpdateAttribute(ref xDoc, fieldNode, "FieldName",fieldName);
                    XmlDataUtil.UpdateAttribute(ref xDoc, fieldNode, "Weight",bestFieldWeights[fieldName].ToString());
                }
                xDoc.Save(this.instruction.ResultFilePath);

                this.output = new SearchFieldContributionOutput(this.instruction.OwnerComponentID, this._TicketID,
                    this.instruction.ResultFilePath, bestFieldWeights);
                if (this.WorkerFinishedEvent != null)
                    this.WorkerFinishedEvent(this._TicketID, this.GetType(), this.instruction, this.output);
            }
            catch(Exception ex)
            {
                if (this.WorkerErredEvent != null)
                    this.WorkerErredEvent(this._TicketID, this.GetType(), this.instruction, ex.ToString(), true);
            }
        }
        #endregion

        #region full text search
        private void DoFullTextSearch(string fieldName,
            ref List<FullTextSearchResult> fieldHits, 
            ref List<FullTextSearchResult> otherFieldHits)
        {
            fieldHits=new List<FullTextSearchResult>();
            otherFieldHits=new List<FullTextSearchResult>();
            SqlConnection conn = new SqlConnection(this.instruction.ConnStr);
            try
            {
                foreach (string txtField in this.instruction.TextFields)
                {
                    if (!this.VerifyFieldIsFullTextIndexed(this.instruction.ConnStr, this.instruction.TableName, txtField))
                        throw new Exception(string.Format("field {0} must be full text enabled", txtField));
                }

                conn.Open();
                int maxHits = 10000;
                fieldHits = DbUtil.DoFreeTextSearch(
                            this.instruction.ConnStr, this.instruction.TableName,
                            this.instruction.KeyField, this.instruction.TitleField,
                            fieldName, this.instruction.Query,
                            maxHits);

                List<string> otherFields=new List<string>();
                foreach(string otherField in this.instruction.TextFields)
                {
                    if(otherField !=fieldName)
                        otherFields.Add(otherField);
                }
                if(otherFields.Count>0)
                {
                    otherFieldHits = DbUtil.DoFreeTextSearch(
                        this.instruction.ConnStr, this.instruction.TableName,
                        this.instruction.KeyField, this.instruction.TitleField,
                        otherFields.ToArray(), this.instruction.Query, maxHits);
                }
            }
            catch (Exception ex)
            {
                if (this.WorkerErredEvent != null)
                    this.WorkerErredEvent(this._TicketID, this.GetType(), this.instruction, ex.ToString(), true);
            }
            finally
            {
                conn.Close();
            }
        }

        private bool VerifyFieldIsFullTextIndexed(string connStr, string tblName, string textFieldName)
        {
            if (DbUtil.IsTableFullTextIndexed(connStr, tblName))
            {
                List<string> fxColNames = DbUtil.GetFullTextIndexedColumnNames(connStr, tblName);
                if (fxColNames != null && fxColNames.Count > 0 && fxColNames.Contains(textFieldName))
                    return true;
            }
            return false;
        }
        #endregion

        #region filter 
        private Dictionary<double, List<string>> FilterAcrossWeights(List<FullTextSearchResult> fieldHits, List<FullTextSearchResult> otherFieldHits)
        {
            Dictionary<double, List<string>> keysFoundAcrossWeights=new Dictionary<double, List<string>>();
            for(double w = 0; w <= 1.0; w +=0.1)
            {
                double w2 = 1 - w;
                Dictionary<string, FullTextSearchResult> allHits=new Dictionary<string, FullTextSearchResult>();
                foreach(FullTextSearchResult hit in fieldHits)
                {
                    if(allHits.ContainsKey(hit.Key.ToString()))
                    {
                        FullTextSearchResult hit1 = allHits[hit.Key.ToString()];
                        hit1.Score += hit.Score*w;
                    }
                    else
                    {
                        hit.Score = hit.Score*w;
                        allHits.Add(hit.Key.ToString(),hit);
                    }
                }
                foreach(FullTextSearchResult hit in otherFieldHits)
                {
                    if (allHits.ContainsKey(hit.Key.ToString()))
                    {
                        FullTextSearchResult hit1 = allHits[hit.Key.ToString()];
                        hit1.Score += hit.Score * w2;
                    }
                    else
                    {
                        hit.Score = hit.Score * w2;
                        allHits.Add(hit.Key.ToString(), hit);
                    }
                }
                double maxRank1 = 0;
                double maxScore1 = 0;
                foreach (FullTextSearchResult hit in allHits.Values)
                {
                    if (hit.Score > maxScore1)
                        maxScore1 = hit.Score;
                    if (hit.Rank > maxRank1)
                        maxRank1 = hit.Rank;
                }
                List<string> keysFound=new List<string>();
                switch(this.instruction.FilterMethod)
                {
                    case FullTextSearchFilterMethod.Score:
                        foreach(FullTextSearchResult hit in allHits.Values)
                        {
                            if(hit.Score >= maxScore1 * this.instruction.FilterThreshold)
                                keysFound.Add(hit.Key.ToString());
                        }
                        break;
                    case FullTextSearchFilterMethod.Rank:
                        foreach (FullTextSearchResult hit in allHits.Values)
                        {
                            if (hit.Rank >= maxRank1 * this.instruction.FilterThreshold)
                                keysFound.Add(hit.Key.ToString());
                        }
                        break;
                    case FullTextSearchFilterMethod.ScoreAndRank:
                        foreach (FullTextSearchResult hit in allHits.Values)
                        {
                            if ((hit.Rank >= maxRank1 * this.instruction.FilterThreshold) &&
                                (hit.Score >= maxScore1 * this.instruction.FilterThreshold))
                                keysFound.Add(hit.Key.ToString());
                        }
                        break;
                    case FullTextSearchFilterMethod.ScoreOrRank:
                        foreach (FullTextSearchResult hit in allHits.Values)
                        {
                            if ((hit.Rank >= maxRank1 * this.instruction.FilterThreshold) ||
                                (hit.Score >= maxScore1 * this.instruction.FilterThreshold))
                                keysFound.Add(hit.Key.ToString());
                        }
                        break;
                    default:
                        break;
                }

                keysFoundAcrossWeights.Add(w, keysFound);
            }

            return keysFoundAcrossWeights;
        }
        #endregion

        #region f-score
        private List<string> GetStandardSetKeys()
        {
            SetDataProvider provider = new SetDataProvider();
            provider.DataSourcePath = this.instruction.StandardSetDataSourcePath;
            provider.RecordListPath = this.instruction.StandardSetRecordPath;
            provider.KeyFieldName = this.instruction.StandardSetKeyField;
            provider.FieldPaths=new Dictionary<string, string>();
            provider.SetName = "Standard";
            provider.SourceType = this.instruction.StandardDataSourceType;
            DataTable dt = provider.GetDataFromProvider();
            List<string> keys=new List<string>();
            if(dt !=null && dt.Rows.Count>0)
            {
                foreach(DataRow dr in dt.Rows)
                {
                    if(dr[this.instruction.StandardSetKeyField]!=null)
                    {
                        keys.Add(dr[this.instruction.StandardSetKeyField].ToString());
                    }
                }
            }
            return keys;
        }

        private double CalculateFScore(List<string> searchResults, List<string> standard)
        {
            if (searchResults == null || searchResults.Count == 0)
                return 0;
            if (standard == null || standard.Count == 0)
                return 0;

            int covered = 0;
            foreach(string keyFound in searchResults)
            {
                if (standard.Contains(keyFound))
                    covered++;
            }
            if (covered == 0)
                return 0;

            double accuracy = ((double) covered)/searchResults.Count;
            double recall = ((double) covered)/standard.Count;
            return 2*accuracy*recall/(accuracy + recall);
        }
        #endregion
    }
}
